/***************************************************************************\
 * The Mattise Kernel: Userland Software									*
 * Copyright 2007 Matthew Iselin											*
 * Licensed under the GPL													*
 *																			*
 * main.c																	*
 *																			*
 * FAT functions															*
 *																			*
\***************************************************************************/

// Includes
#include <stdio.h>
#include "fat.h"

// Constants
#define FILENAME_MAXLENGTH		255

// Functions

// gets a substring from a larger string. TODO: make faster, and safer
char* substr( char* str, int start, int end )
{
	// get some space for the returned pointer
	char* ret = (char*) malloc( end - start );
	
	// do the fill
	int i;
	for( i = start; i < end; i++ )
	{
		ret[i-start] = str[i];
	}
	ret[i-start] = 0;
	
	// return it - make sure it points to the right place though
	return ret;
}

// finds a character in a string. TODO: make faster, safer
int find( char* str, int start, int end, char c )
{
	// loop through the string
	if( start < end )
	{
		int i;
		for( i = start; i < end; i++ )
			if( str[i] == c )
				return i;
	}
	else
	{
		int i;
		for( i = start; i > end; i-- )
			if( str[i] == c )
				return i;
	}

	// none found
	return -1;
}

// FAT: Gets the first sector in a cluster
int FirstSectorOfCluster( fat32_filedesc* fd, struct BPB_FAT bpb, int n )
{		
	// return the value
	return ( ( n - 2 ) * bpb.BPB_SecPerClus ) + fd->DataStart;
}

// Gets the offset and sector number of a cluster in the FAT
void WhereInFat( fat32_filedesc* fd, struct BPB_FAT bpb, int clus, int* sec, int* off )
{
	// variables for later
	uint_t FATOffset = clus * 4;
	
	// sector number and offset in the FAT
	*sec = bpb.BPB_RsvdSecCnt + ( FATOffset / bpb.BPB_BytsPerSec );
	*off = FATOffset % bpb.BPB_BytsPerSec;
}

// FAT32: Gets the value of a cluster entry
uint_t fat32_GetClusterEntry( fat32_filedesc* fd, struct BPB_FAT bpb, int clus )
{
	// variables
	uint_t fatsect = 0, offset = 0;
	
	// get where it is in the FAT
	WhereInFat( fd, bpb, clus, &fatsect, &offset );

	// entry value to return
	uint_t FatEntryVal = 0;
	
	// FAT sector
	char myfat[512];
	
	// read the sector
	lseek( fd->devdesc, fatsect, SEEK_SET );
	read( fd->devdesc, myfat, 1 );
	
	// get the value - top 4 bits are reserved
	FatEntryVal = (*((uint_t*) &myfat[offset])) & 0x0FFFFFFF;
	
	// return it
	return FatEntryVal;
}

// FAT32: Sets the data at the given cluster offset
uint_t fat32_SetClusterEntry( fat32_filedesc* fd, struct BPB_FAT bpb, uint_t clus, uint_t data )
{
	// variables
	uint_t fatsect = 0, offset = 0;
	
	// get where it is in the FAT
	WhereInFat( fd, bpb, clus, &fatsect, &offset );

	// entry value to return
	uint_t FatEntryVal = 0;
	
	// FAT sector
	char myfat[512];
	
	// read the sector
	lseek( fd->devdesc, fatsect, SEEK_SET );
	read( fd->devdesc, myfat, 1 );
	
	// set the new entry - top 4 bits are reserved
	FatEntryVal = clus & 0x0FFFFFFF;
	*((uint_t*) &myfat[offset]) = (*((uint_t*) &myfat[offset])) & 0xF0000000;
	*((uint_t*) &myfat[offset]) = (*((uint_t*) &myfat[offset])) | FatEntryVal;
	
	// write the sector
	lseek( fd->devdesc, fatsect, SEEK_SET );
	write( fd->devdesc, myfat, 1 );
	
	// return it
	return FatEntryVal;
}

// Appends a cluster chain to the given cluster. Basically, this will make currClust point to endClust and will return whatever it writes
int fat32_AppendCluster( fat32_filedesc* fd, struct BPB_FAT bpb, uint_t currClust, uint_t endClust )
{
	// return value
	int ret = -1;
	
	// set the clusters
	ret = fat32_SetClusterEntry( fd, bpb, currClust, endClust );
	if( ret < 0 ) return ret;
	ret = fat32_SetClusterEntry( fd, bpb, endClust, 0xFFFFFFF );
	return ret;
}

// FAT32: Finds a free cluster in the FAT. This will return either the start of a new cluster chain or the end of an old cluster chain
uint_t fat32_FindFreeCluster( fat32_filedesc* fd, struct BPB_FAT bpb, uint_t startclus )
{
	// search
	int j;
	uint_t clus = 0;
	for( j = startclus; j < bpb.BPB_TotSec32; j++ )
	{
		// get the data
		clus = fat32_GetClusterEntry( fd, bpb, j );
		
		// is it a null entry?
		if( !clus )
			break;
		
		// valid start cluster?
		if( startclus )
		{
			fat32_AppendCluster( fd, bpb, startclus, clus );
		}
		else
		{
			// create it...
		}
	}
	
	// return it
	return clus;
}

// FAT32: Gets the number of clusters at a specific location by parsing the FAT
uint_t fat32_NumClusters( fat32_filedesc* fd, struct BPB_FAT bpb, uint_t clus )
{
	// count
	uint_t num = 0;
	
	// get the first cluster entry
	uint_t dat = fat32_GetClusterEntry( fd, bpb, clus );
	
	// read in while not eof
	while( ! ( dat >= 0x0FFFFFF8 ) )
	{
		dat = fat32_GetClusterEntry( fd, bpb, dat );
		num++;
	}
	
	// all done, return the number of clusters
	return num + 1;
}

// FAT32: Gets the cluster for a specific file position
void fat32_GetClusterFilePos( fat32_filedesc* fd, struct BPB_FAT bpb, uint_t pos, uint_t* cluster, uint_t* cluster_pos )
{
	// indexes
	int i,j = 0;
	
	// the block size
	uint_t blockSize = bpb.BPB_SecPerClus * bpb.BPB_BytsPerSec;
	
	// loop through each entry until we hit it
	uint_t clus = fat32_GetClusterEntry( fd, bpb, ( fd->myDirEnt.DIR_FstClusHI << 16 ) | fd->myDirEnt.DIR_FstClusLO );
	while( ! ( clus > 0x0FFFFFF8 ) )
	{
		if( j * blockSize <= pos && ((j+1)*blockSize) > pos )
			break;
		j++;
		clus = fat32_GetClusterEntry( fd, bpb, clus );
	}
	*cluster = clus;
	*cluster_pos = j * blockSize;
}

// FAT32: Searches a directory for a specific filename (8.3 format only)
uint_t fat32_SearchDir( fat32_filedesc* fd, struct BPB_FAT bpb, struct BPB_FAT32 bpb32, char* fn, int dirclus )
{
	// get the sector of the directory
	uint_t dirsec = FirstSectorOfCluster( fd, bpb, dirclus );
	
	// number of sectors. TODO: change this to read in more than one cluster
	uint_t numsect = bpb.BPB_SecPerClus;
	
	// number of bytes
	uint_t numbytes = numsect * bpb.BPB_BytsPerSec;
	
	// read in the cluster
	uchar_t* buff = (uchar_t*) malloc( numbytes );
	lseek( fd->devdesc, dirsec, SEEK_SET );
	int secoff = 0;
	while( secoff < numbytes )
	{
		read( fd->devdesc, (uchar_t*) (buff+secoff), 1 );
		secoff += bpb.BPB_BytsPerSec;
	}
	
	// get info
	int i;
	for( i = 0; i < numbytes; i += 32 )
	{
		// check for a valid file
		if( buff[i] == 0xE5 )
		{
			continue;
		}
		if( buff[i] == 0x00 )
		{
			break;
		}
		
		// get the data of this entry
		struct FAT32_DirEnt myEnt = *((struct FAT32_DirEnt*) &buff[i]);

		// check it
		if( myEnt.DIR_Name[0] == '.' ) { continue; };
		if( myEnt.DIR_Attr & ATTR_VOLUME_ID ) // don't let the volume ID be read in as the file we're looking for!
			continue;
		if( strncmp( (char*) myEnt.DIR_Name, fn, strlen( fn ) ) == 0 )
		{
			fd->myDirEnt = myEnt;
			fd->diroffset = i;
			free( buff );
			return ( myEnt.DIR_FstClusHI << 16 ) | myEnt.DIR_FstClusLO;
		}
	}
	free( buff );
	return 0;
}

/// Pads a short filename (and will just return the filename if it doesn't need to be padded)
char* PadFilename( char* str )
{
	// check, is it more than 11 bytes?
	if( strlen( str ) > 11 )
	{
		// long filename!
		return str;
	}
	
	// counters and the temp string (that will hold	the new filename)
	uint_t count = 0, tmp_count = 0;
	char tmpstring[11];
	memset( tmpstring, 0, 11 );
	
	// loop through each character in the string
	for( tmp_count = 0; tmp_count < 11; tmp_count++, count++ )
	{
		// if it's not a dot, put it in the string
		if( str[count] != '.' && str[count] != 0 )
		{
			tmpstring[tmp_count] = toupper( str[count] );
		}
		else
		{
			// space out the string
			for( ; tmp_count < 8; tmp_count++ )
			{
				tmpstring[tmp_count] = ' ';
			}
			
			// put in the extension
			if( str[count+1] != 0 )
			{
				tmpstring[tmp_count] = toupper( str[count+1] );
				tmpstring[tmp_count+1] = toupper( str[count+2] );
				tmpstring[tmp_count+2] = toupper( str[count+3] );
			}
			else
			{
				tmpstring[tmp_count] = ' ';
				tmpstring[tmp_count+1] = ' ';
				tmpstring[tmp_count+2] = ' ';
			}
			
			// break from the main loop
			break;
		}
	}
	
	// copy the temp string into the main one
	tmpstring[11] = 0;
	memcpy( str, tmpstring, 12 );
	
	// return it
	return str;
}

// FAT32 open function
int fat32_open( fat32_filedesc* fd, struct BPB_FAT bpb, struct BPB_FAT32 bpb32, char* fn )
{
	// we need the path and filename (fd->devdesc is already set)
	char* path;
	char* filename;
	
	printf( "Searching in filename [%s]...\n", fn );
	
	// find the first slash in the path (Mattise supports both directions)
	int firstslash = find( fn, 0, strlen( fn ), '/' );
	if( firstslash == -1 )
		firstslash = find( fn, 0, strlen( fn ), '\\' );
	if( firstslash == -1 )
		return -1;
	
	// get the last slash
	int lastslash = find( fn, strlen( fn ), firstslash, '/' );
	if( lastslash == -1 )
		lastslash = find( fn, strlen( fn ), firstslash, '\\' );
	
	// find out if it's just a filename
	if( lastslash == -1 )
		filename = substr( fn, firstslash + 1, strlen( fn ) );
	else
	{
		path = substr( fn, firstslash + 1, lastslash );
		filename = substr( fn, lastslash + 1, strlen( fn ) );
	}
	
	printf( "Searching directories\n" );
	
	// now work through from the root directory, searching all directories in the path
	// to find the final cluster of the directory to search in
	uint_t dirclus = bpb32.BPB_RootClus;
	int tmpslash = 0;
	if( lastslash != -1 )
	{
		// get the relevant part of the path - this is not as complex as it looks
		while( tmpslash != -1 && !(tmpslash == strlen( path )) )
		{
			int nslash = find( path, tmpslash, strlen( path ), '/' );
			if( nslash == -1 )
				nslash = find( path, tmpslash, strlen( path ), '\\' );
			if( nslash == -1 )
				nslash = strlen( path );
			char* tmp = (char*) malloc( nslash - tmpslash );
			tmp = substr( path, tmpslash, nslash );
			tmp = PadFilename( tmp );
			dirclus = fat32_SearchDir( fd, bpb, bpb32, tmp, dirclus );
			free( tmp );
			tmpslash = nslash;
		}
	}
	
	// convert to an 8.3 filename
	filename = PadFilename( filename );
	
	printf( "Padded filename [%d]\n", dirclus );
	
	// search for it
	uint_t searchres = fat32_SearchDir( fd, bpb, bpb32, filename, dirclus );
	
	printf( "Searched!\n" );
	
	// check if it's valid
	if( searchres == 0 )
	{
		return -1;
	}
	
	// return that we got it!
	return 0;
}

// FAT32 read function
int fat32_read( fat32_filedesc* fd, struct BPB_FAT bpb, struct BPB_FAT32 bpb32, int offset, int count, char* buff )
{
	// check for a valid device descriptor
	if( fd->devdesc == -1 )
		return -1;
	
	// get a pointer to our data
	fat32_filedesc* ptr = fd;
	
	// the cluster of the file
	uint_t fileclus = ( fd->myDirEnt.DIR_FstClusHI << 16 ) | fd->myDirEnt.DIR_FstClusLO;
	
	// we need to firsly check for a sector boundary break
	
	// the final offset into the file
	uint_t totbytes = offset + count;
	
	// the sector of the offset's sector
	uint_t offsect = offset / bpb.BPB_BytsPerSec;
	
	// sector of the final offset
	uint_t allsect = totbytes / bpb.BPB_BytsPerSec;
	
	// number of bytes read
	uint_t nb = 0;

	// find out which sector (non-relative)
	uint_t realsect = FirstSectorOfCluster( fd, bpb, fileclus ) + offsect;
	
	// have we enough clusters?
	int i = bpb.BPB_SecPerClus * bpb.BPB_BytsPerSec;
	int j = ptr->myDirEnt.DIR_FileSize / i;
	if( ptr->myDirEnt.DIR_FileSize % i )
		j++;
	if( j == 0 )
		j = 1; // we always have one cluster
	
	// get endpoints
	int endPosCluster = j * i; // the end cluster offset
	int endPosFile = offset + count; // the end of our write area
	int numExtraBytes = 0;
	
	// do we need more clusters?
	if( endPosFile > endPosCluster )
	{
		// oops, can't read past EOF
		return -1;
	}
	
	// cluster position and number
	uint_t clusterPos,fileCluster;
	fat32_GetClusterFilePos( fd, bpb, offset, &clusterPos, &fileCluster );
	
	// set the skips
	uint_t sectorSkip = ( offsect % bpb.BPB_SecPerClus );
	uint_t clusterSkip = offsect / bpb.BPB_SecPerClus;
	uint_t dataSkip = offset % bpb.BPB_BytsPerSec;
	
	// the current position in the buffer
	uint_t bufferPos = 0;
	
	// get the cluster chain
	uint_t cluschain[4096]; // maximum of 4096 links
	int cluscnt = 0; // there will always be 1 - EOF
	cluschain[cluscnt++] = fileclus;
	uint_t myclus = fat32_GetClusterEntry( fd, bpb, fileclus );
	while( ! ( myclus > 0x0FFFFFF8 ) )
	{
		cluschain[cluscnt++] = myclus;
		myclus = fat32_GetClusterEntry( fd, bpb, myclus );
	}
	cluschain[cluscnt++] = myclus;
	
	// sector buffer
	char sec[512];
	uint_t bufSize = 512;
	
	// loop through each cluster
	for( i = ( 0 + clusterSkip ); i < cluscnt && count > 0; i++ )
	{
		// the start of this cluster
		uint_t startSector = FirstSectorOfCluster( fd, bpb, cluschain[i] );
		
		// process every sector
		for( j = 0 + sectorSkip; j < bpb.BPB_SecPerClus && count > 0; j++ )
		{
			// read in the sector
			lseek( fd->devdesc, startSector + j, SEEK_SET );
			read( fd->devdesc, sec, 1 );
			
			// check that we have room
			if( count < bufSize )
				bufSize = count + dataSkip;
			if( bufSize > 512 )
				bufSize = 512;
			
			// copy the data
			char* ptr = (char*) buff;
			memcpy( (char*) (ptr+bufferPos), (char*) (sec + dataSkip), bufSize - dataSkip );
			
			// decrement the size
			count -= (bufSize - dataSkip);
			bufferPos += (bufSize - dataSkip);
			dataSkip = 0;
			bufSize = 512;
		}
		sectorSkip = 0;
	}
	
	// return the number of bytes written
	return bufferPos;
}

// FAT32 write function
int fat32_write( fat32_filedesc* fd, struct BPB_FAT bpb, struct BPB_FAT32 bpb32, int offset, int count, char* buff )
{
	// check for a valid device descriptor
	if( fd->devdesc == -1 )
		return -1;
	
	// get a pointer to our data
	fat32_filedesc* ptr = fd;
	
	// the cluster of the file
	uint_t fileclus = ( fd->myDirEnt.DIR_FstClusHI << 16 ) | fd->myDirEnt.DIR_FstClusLO;
	
	// we need to firsly check for a sector boundary break
	
	// the final offset into the file
	uint_t totbytes = offset + count;
	
	// the sector of the offset's sector
	uint_t offsect = offset / bpb.BPB_BytsPerSec;
	
	// sector of the final offset
	uint_t allsect = totbytes / bpb.BPB_BytsPerSec;
	
	// number of bytes read
	uint_t nb = 0;

	// find out which sector (non-relative)
	uint_t realsect = FirstSectorOfCluster( fd, bpb, fileclus ) + offsect;
	
	// have we enough clusters?
	int i = bpb.BPB_SecPerClus * bpb.BPB_BytsPerSec;
	int j = ptr->myDirEnt.DIR_FileSize / i;
	if( ptr->myDirEnt.DIR_FileSize % i )
		j++;
	if( j == 0 )
		j = 1; // we always have one cluster
	
	// get endpoints
	int endPosCluster = j * i; // the end cluster offset
	int endPosFile = offset + count; // the end of our write area
	int numExtraBytes = 0;
	
	// do we need more clusters?
	if( endPosFile > endPosCluster )
	{
		// how many extra bytes do we need?
		numExtraBytes = endPosFile - endPosCluster;
		
		// convert to clusters
		j = numExtraBytes / i;
		if( numExtraBytes % i )
			j++;
		
		// get the last cluster
		int lastCluster = 0;
		uint_t clus = fat32_GetClusterEntry( fd, bpb, fileclus );
		while( ! ( clus >= 0x0FFFFFF8 ) )
		{
			// increment our count
			lastCluster = clus;
			
			// get the next entry
			clus = fat32_GetClusterEntry( fd, bpb, fileclus );
		}
		
		// now set the new last cluster
		for( i = 0; i < j; i++ )
		{
			lastCluster = fat32_FindFreeCluster( fd, bpb, lastCluster );
			// TODO: handle no more left!
		}
	}
	
	// cluster position and number
	uint_t clusterPos,fileCluster;
	fat32_GetClusterFilePos( fd, bpb, offset, &clusterPos, &fileCluster );
	
	// set the skips
	uint_t sectorSkip = ( offsect % bpb.BPB_SecPerClus ); // ( offset % drvBPB[fp.dev].BPB_BytsPerSec ) % drvBPB[fp.dev].BPB_SecPerClus; //(offset - clusterPos) / drvBPB[fp.dev].BPB_BytsPerSec;
	uint_t clusterSkip = offsect / bpb.BPB_SecPerClus; //offset % (drvBPB[fp.dev].BPB_SecPerClus*drvBPB[fp.dev].BPB_BytsPerSec); // sectorSkip / drvBPB[fp.dev].BPB_SecPerClus;
	uint_t dataSkip = offset % bpb.BPB_BytsPerSec;
	
	// the current position in the buffer
	uint_t bufferPos = 0;
	
	// get the cluster chain
	uint_t cluschain[4096]; // maximum of 4096 links
	int cluscnt = 0; // there will always be 1 - EOF
	cluschain[cluscnt++] = fileclus;
	uint_t myclus = fat32_GetClusterEntry( fd, bpb, fileclus );
	while( ! ( myclus > 0x0FFFFFF8 ) )
	{
		cluschain[cluscnt++] = myclus;
		myclus = fat32_GetClusterEntry( fd, bpb, myclus );
	}
	cluschain[cluscnt++] = myclus;
	
	// sector buffer
	char sec[512];
	uint_t bufSize = 512;
	
	// loop through each cluster
	for( i = ( 0 + clusterSkip ); i < cluscnt && count > 0; i++ )
	{
		// the start of this cluster
		uint_t startSector = FirstSectorOfCluster( fd, bpb, cluschain[i] );
		
		// process every sector
		for( j = 0 + sectorSkip; j < bpb.BPB_SecPerClus && count > 0; j++ )
		{
			// read in the sector
			lseek( fd->devdesc, startSector + j, SEEK_SET );
			read( fd->devdesc, sec, 1 );
			
			// check that we have room
			if( count < bufSize )
				bufSize = count + dataSkip;
			if( bufSize > 512 )
				bufSize = 512;
			
			// copy the data
			char* ptr = (char*) buff;
			memcpy( (char*) (sec + dataSkip), (char*) (ptr+bufferPos), bufSize - dataSkip );
			
			// write the data
			lseek( fd->devdesc, startSector + j, SEEK_SET );
			write( fd->devdesc, sec, 1 );
			
			// decrement the size
			count -= (bufSize - dataSkip);
			bufferPos += (bufSize - dataSkip);
			dataSkip = 0;
			bufSize = 512;
		}
		sectorSkip = 0;
	}
	
	// update the directory entry, if needed
	if( bufferPos )
	{
		// difference between the old size and the new size
		numExtraBytes = ptr->myDirEnt.DIR_FileSize - offset;
		
		// check if it's more than 0
		if( numExtraBytes > 0 )
		{
			// read in the directory sector
			lseek( fd->devdesc, ptr->dirsect, SEEK_SET );
			read( fd->devdesc, sec, 1 );
			
			// set the new entry
			struct FAT32_DirEnt* myEnt = (struct FAT32_DirEnt*) &sec[ptr->diroffset];
			ptr->myDirEnt.DIR_FileSize += numExtraBytes;
			*myEnt = ptr->myDirEnt;
			
			// write the data back
			lseek( fd->devdesc, ptr->dirsect, SEEK_SET );
			write( fd->devdesc, sec, 1 );
		}
	}

	// return the new buffer position
	return bufferPos;
}
